# -*- coding: utf-8 -*-
import os
import json
import contextlib
import math
import random
import retrying
import argparse

import numpy as np
from bs4 import BeautifulSoup
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.common.action_chains import ActionChains
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
import pandas as pd
import regex as re
import time

from selenium.webdriver.support.select import Select
from selenium.webdriver.remote.webdriver import WebDriver, WebElement
from datetime import datetime

from howlongtobeatpy import HowLongToBeat, SearchModifiers
import matplotlib.pyplot as plt

plt.rcParams["font.family"] = ["Smiley Sans", "sans-serif"]
plt.rcParams["font.size"] = 15

from logger import setup_logger

llog = setup_logger(name=__name__)

RETRY_NUMS = 10


def parse_args():
    parser = argparse.ArgumentParser(description="Steam Scraper")
    parser.add_argument("--game_link", type=str, help="游戏的Steam主页链接")
    args = parser.parse_args()
    return args


def convert_valid_name(name: str, invalid_char: str = r"[^\w\s.-]", replace: str = "") -> str:
    return re.sub(invalid_char, replace, name)


def read_json_file(file, **open_kwargs):
    with open(file, "r", **open_kwargs) as f:
        out = json.load(f)
    return out


def write_json_file(file, obj, override=False, min_size=10, encoding="utf-8", **dump_kwargs):
    if os.path.exists(file) and not override:
        raise RuntimeError(f"Try to override file with override={override}.")
    dump_cont = json.dumps(obj, **dump_kwargs)
    if len(dump_cont) <= min_size:
        raise RuntimeError(f"Dump object size is smaller than {min_size}, which is not expected.")
    with open(file, "w+", encoding=encoding) as f:
        f.write(dump_cont)


@contextlib.contextmanager
def timer(line):
    start = time.time()
    yield
    end = time.time()
    llog.info(f"time right now: {datetime.now()}")
    llog.info(f"{line} + elapsed time: {end - start}s")


def passing_age_page(driver: WebDriver):
    try:
        year = driver.find_element(By.ID, "ageYear")
        dropdown = Select(year)
        dropdown.select_by_value("1999")
        view_page_button = driver.find_element(By.ID, "view_product_page_btn")
        driver.execute_script("arguments[0].click()", view_page_button)
        time.sleep(1)
    except:
        pass


# 获取评论数
@retrying.retry(stop_max_attempt_number=RETRY_NUMS, wait_random_min=1500, wait_random_max=2300)
def get_review_filter_score(driver: WebDriver) -> int:
    review_filter_score_str = driver.find_element(By.ID, "user_reviews_filter_score").text
    review_filter_score = int(re.findall(r"\d+", review_filter_score_str.replace(",", ""))[0])
    return review_filter_score


# 打开主页，顺便过一下年龄认证
@retrying.retry(stop_max_attempt_number=RETRY_NUMS, wait_fixed=2000)
def open_game_homepage(driver: WebDriver, game_link: str):
    driver.get(game_link)
    passing_age_page(driver)


def search_game_homepage(driver: WebDriver, game_name: str, lajiyouxi_meiyingwen: dict):
    driver.execute_script("window.scrollTo(0, 0);")

    time.sleep(random.random())

    # 输入游戏名
    search_input = driver.find_element(By.ID, "store_nav_search_term")
    driver.execute_script("arguments[0].click()", search_input)

    # 等待
    search_name = (
        game_name if game_name not in lajiyouxi_meiyingwen else lajiyouxi_meiyingwen[game_name]
    )
    try:
        search_input.send_keys(search_name)
        suggestion_contents = WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.ID, "search_suggestion_contents"))
        )
    except:
        driver.execute_script("arguments[0].click()", search_input)
        search_input.clear()
        search_input.send_keys(re.sub("[^a-zA-Z0-9 ]", "", search_name))
        suggestion_contents = WebDriverWait(driver, 10).until(
            EC.visibility_of_element_located((By.ID, "search_suggestion_contents"))
        )

    # 点击最上方的推荐进入游戏主页
    top_match = suggestion_contents.find_elements(By.CLASS_NAME, "match_category_top")
    top_match[0].click()

    time.sleep(random.random() + 1)
    passing_age_page(driver)


def get_game_description(driver, link):
    soup = BeautifulSoup(driver.page_source, "lxml")
    game_description = {
        "name": soup.find_all("div", class_="apphub_AppName")[0].text.strip()
        if soup.find_all("div", class_="apphub_AppName") is not None
        else "",
        "short_description": soup.find("div", class_="game_description_snippet").text.strip()
        if soup.find("div", class_="game_description_snippet") is not None
        else "",
        "long_description": soup.find_all("div", id="aboutThisGame")[0].text.strip()
        if soup.find_all("div", id="aboutThisGame") is not None
        else "",
        "genres": [],
        "minimum_system_requirement": [],
        "recommend_system_requirement": [],
        "release_date": soup.find("div", class_="release_date").find("div", class_="date").text,
        "developer": [
            developer.text.strip()
            for developer in soup.find("div", class_="glance_ctn_responsive_left")
            .find_all("div", class_="dev_row")[0]
            .find_all("a")
        ],
        "publisher": [
            developer.text.strip()
            for developer in soup.find("div", class_="glance_ctn_responsive_left")
            .find_all("div", class_="dev_row")[1]
            .find_all("a")
        ],
        "overall_player_rating": "",
        "number_of_reviews_from_purchased_people": "",
        "number_of_english_reviews": soup.find(
            "div", class_="user_reviews_filter_score visible"
        ).text.split()[1]
        if soup.find("div", class_="user_reviews_filter_score visible") is not None
        else "",
        "link": link,
    }

    user_reviews = ""
    if len(soup.find_all("div", class_="user_reviews_summary_row")) != 0:
        user_reviews = soup.find_all("div", class_="user_reviews_summary_row")[1]
    elif len(soup.find_all("a", class_="user_reviews_summary_row")) != 0:
        user_reviews = soup.find_all("a", class_="user_reviews_summary_row")[1]
    
    if len(user_reviews) != 0:
        game_description["overall_player_rating"] = user_reviews.find("span", class_=re.compile("game_review_summary")).text.strip()
        game_description["number_of_reviews_from_purchased_people"] = user_reviews.find_all("span")[1].text.strip()    

    genre = soup.find("div", class_="glance_tags popular_tags")
    if genre is None:
        game_description["genres"] = []
    else:
        genres = genre.find_all("a", class_="app_tag")
        if genres is not None:
            for i in genre.find_all("a", class_="app_tag"):
                game_description["genres"].append(i.text.strip())
        else:
            game_description["genres"] = []

    minimum_system_requirement = soup.find(
        "div", class_=re.compile("game_area_sys_req_leftCol|game_area_sys_req_full")
    )
    if minimum_system_requirement is None:
        game_description["minimum_system_requirement"] = []
    else:
        minimum_system_requirement = minimum_system_requirement.find("ul", class_="bb_ul")
        if minimum_system_requirement is None:
            game_description["minimum_system_requirement"] = []
        else:
            minimum_system_requirement = minimum_system_requirement.find_all("li")
            if minimum_system_requirement is None:
                game_description["minimum_system_requirement"] = []
            else:
                for i in minimum_system_requirement:
                    game_description["minimum_system_requirement"].append(i.text.strip())

    recommend_system_requirement = soup.find(
        "div", class_=re.compile("game_area_sys_req_rightCol|game_area_sys_req_full")
    )
    if recommend_system_requirement is None:
        game_description["recommend_system_requirement"] = []
    else:
        recommend_system_requirement = recommend_system_requirement.find("ul", class_="bb_ul")
        if recommend_system_requirement is None:
            game_description["recommend_system_requirement"] = []
        else:
            recommend_system_requirement = recommend_system_requirement.find_all("li")
            if recommend_system_requirement is None:
                game_description["recommend_system_requirement"] = []
            else:
                for i in recommend_system_requirement:
                    game_description["recommend_system_requirement"].append(i.text.strip())
    llog.info(game_description)
    return game_description


def hover_on_element(driver: WebDriver, element: WebElement):
    action = ActionChains(driver).click_and_hold(element).release().move_to_element(element)
    action.perform()


def drag_slider(driver: WebDriver, handle: WebElement, offset: int):
    action = ActionChains(driver).click_and_hold(handle).move_by_offset(offset, 0).release()
    action.perform()


def get_one_reviews_num(driver: WebDriver, handle: WebElement, hour: int, offset: int) -> int:
    drag_slider(driver, handle, offset)
    time.sleep(0.75 + random.random() / 4)
    current_hour = int(re.findall(r"\d+", handle.get_attribute("style"))[0])
    while current_hour != hour:
        if current_hour < hour:
            offset += 1
            drag_slider(driver, handle, offset)
            time.sleep(0.5 + random.random() / 2)
            current_hour = int(re.findall(r"\d+", handle.get_attribute("style"))[0])
        elif current_hour > hour:
            offset -= 1
            drag_slider(driver, handle, offset)
            time.sleep(0.5 + random.random() / 2)
            current_hour = int(re.findall(r"\d+", handle.get_attribute("style"))[0])

    return get_review_filter_score(driver)


# 没加锁，多进程不保证稳定
def set_game_metadata(game_name: str, game_description: dict, avg_ei_scores: float, ei_dataset_path: str):
    valid_game_name = convert_valid_name(game_name)

    # 顺便获取 howlongtobeat 数据
    hltb = HowLongToBeat().search(game_name, search_modifiers=SearchModifiers.HIDE_DLC)
    if len(hltb) == 0:
        hltb = HowLongToBeat().search(valid_game_name)
    if len(hltb) == 0:
        hltb = HowLongToBeat().search(valid_game_name, similarity_case_sensitive=False)

    metadata_dict = read_json_file("games_metadata.json", encoding="utf-8")
    ori_size = metadata_dict.__sizeof__()

    if valid_game_name in metadata_dict:
        ymd_date_str = time.strftime("%Y%m%d")
        llog.warning(f"{valid_game_name} data is already existed in metadata_dict, exist...")
        return

    metadata_dict[valid_game_name] = game_description
    metadata_dict[valid_game_name]["EI_dataset"] = "EI_dataset/" + os.path.basename(ei_dataset_path)
    metadata_dict[valid_game_name]["EI_avg_scores"] = avg_ei_scores
    if len(hltb) != 0:
        metadata_dict[valid_game_name]["hltb_game_name"] = hltb[0].game_name
        metadata_dict[valid_game_name]["hltb_game_image_url"] = hltb[0].game_image_url
        metadata_dict[valid_game_name]["hltb_main_story"] = hltb[0].main_story
        metadata_dict[valid_game_name]["hltb_main_extra"] = hltb[0].main_extra
        metadata_dict[valid_game_name]["hltb_completionist"] = hltb[0].completionist
        metadata_dict[valid_game_name]["hltb_all_styles"] = hltb[0].all_styles
        metadata_dict[valid_game_name]["hltb_review_score"] = hltb[0].review_score
        metadata_dict[valid_game_name]["hltb_release_world"] = hltb[0].release_world
        metadata_dict[valid_game_name]["hltb_profile_platforms"] = hltb[0].profile_platforms
    else:
        llog.warning("Can't find this game in howlongtobeat, missing hltb_* data.")

    new_size = metadata_dict.__sizeof__()
    if metadata_dict.__sizeof__() >= ori_size:
        write_json_file(
            "games_metadata.json", metadata_dict, override=True, indent=4, ensure_ascii=False
        )
    else:
        llog.warning(
            f"New games_metadata ({new_size}) size lower than origin size ({ori_size}), adding suffix '_check'"
        )
        write_json_file(
            "games_metadata_check.json", metadata_dict, override=True, indent=4, ensure_ascii=False
        )


## 等待的代码尚未完善，网络差时可能报错或出现数据点跃动
def main(opts):
    options = webdriver.FirefoxOptions()
    driver = webdriver.Firefox(options=options)

    # 打开游戏英文主页获取名字
    game_link = opts.game_link + "?l=english"
    open_game_homepage(driver, game_link)
    game_name = driver.find_element(By.ID, "appHubAppName").text

    # 切换回中文语言
    driver.execute_script("ChangeLanguage( 'schinese' ); return false;")
    time.sleep(random.random() * 1 + 5)  # 网络不好这里之后可能会报错

    # 滚动到游戏时间菜单
    element_present = EC.presence_of_element_located((By.ID, "reviews_filter_options"))
    reviews_filter_options = WebDriverWait(driver, 10).until(element_present)
    driver.execute_script("arguments[0].scrollIntoView(true);", reviews_filter_options)

    # 显示所有语言
    user_reviews_filter_menus = reviews_filter_options.find_elements(
        By.CLASS_NAME, "user_reviews_filter_menu"
    )
    hover_on_element(driver, user_reviews_filter_menus[2])
    review_language_all_button = driver.find_element(By.ID, "review_language_all")
    driver.execute_script("arguments[0].click()", review_language_all_button)

    time.sleep(random.random() * 1 + 5)  # 网络不好这里之后可能会报错

    # 获取游戏简介
    game_description = get_game_description(driver, game_link)

    # 悬停滑条菜单
    hover_on_element(driver, user_reviews_filter_menus[4])

    # 定位滑条
    app_reviews_playtime_slider = driver.find_element(By.ID, "app_reviews_playtime_slider")
    handles = app_reviews_playtime_slider.find_elements(By.CLASS_NAME, "ui-slider-handle")
    left_handle = handles[0]
    right_handle = handles[1]

    # 每增加 1 小时游玩时长，统计一次评论数
    slider_width = app_reviews_playtime_slider.size["width"]
    offsets = np.int32(np.trunc(np.linspace(0, slider_width, 101))).tolist()
    reviews_nums = [get_review_filter_score(driver)]
    reviews_hours = [0]
    for perc in range(100):
        hour = perc + 1
        try:
            new_reviews_num = get_one_reviews_num(
                driver, left_handle, hour, offsets[hour] - offsets[hour - 1]
            )
        except:
            time.sleep(5)
            hover_on_element(driver, user_reviews_filter_menus[4])
            new_reviews_num = get_one_reviews_num(
                driver, left_handle, hour, offsets[hour] - offsets[hour - 1]
            )
        reviews_nums.append(new_reviews_num)
        reviews_hours.append(hour)
        time.sleep(random.random() / 3)

    avg_ei_scores = 100.0 * np.mean(reviews_nums) / reviews_nums[0]
    df = pd.DataFrame(data=[reviews_hours, reviews_nums], index=["hour", "num"]).T
    valid_file_name = convert_valid_name(game_name)
    out_file_path = "EI_dataset/" + valid_file_name + "_reviews_" + time.strftime("%Y%m%d") + ".csv"
    df.to_csv(out_file_path, index=False)

    # 更新元数据
    set_game_metadata(game_name, game_description, avg_ei_scores, out_file_path)

    driver.quit()


if __name__ == "__main__":
    opts = parse_args()
    with timer("full process"):
        main(opts)
